package ClientsAndHotels;

import jade.core.AID;
import jade.core.behaviours.CyclicBehaviour;
import jade.core.behaviours.TickerBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.lang.acl.UnreadableException;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import ClientsAndHotels.types.DiedAgentData;
import ClientsAndHotels.types.HotelParams;
import ClientsAndHotels.types.OrderRequest;
import ClientsAndHotels.types.OrderResponse;
import ClientsAndHotels.types.Reservation;
import ClientsAndHotels.types.Room;
import java.util.*;

public class HotelAgent extends CalendarObserverAgent {

    public static final String ACTION_CANCEL_RESERVATION = "cancel-reservation";
    public static final String MESSAGE_GOODBYE = "goodbye";
    public static final String MESSAGE_HELLO = "hello";
    private String cityName;
    private int stars;
    private Map<Date, Map<AID, Room>> roomsBooked;
    private ArrayList<Room> rooms;

    @Override
    protected void setup() {

        notifyGuiAboutDie(DiedAgentData.TYPE_HOTEL);

        log("Przybył nowy hotel " + getAID().getName() + ".");
        Object[] args = getArguments();
        if (args.length == 1 && args[0] instanceof HotelParams) {
            HotelParams params = (HotelParams) args[0];
            cityName = params.getCity();
            stars = params.getStars();
            roomsBooked = new HashMap<>();
            rooms = params.getRooms();

            addBehaviour(new TickerBehaviour(this, 1000) {

                @Override
                public void onTick() {
                    // Znajdź pośrednika
                    DFAgentDescription template = new DFAgentDescription();
                    ServiceDescription sd = new ServiceDescription();
                    sd.setType(MediatorAgent.TYPE);
                    template.addServices(sd);
                    try {
                        DFAgentDescription[] result = DFService.search(myAgent,
                                template);
                        if (result.length > 0) {
                            // log("Znaleziono następujących pośredników:");
                            ACLMessage hello = new ACLMessage(ACLMessage.INFORM);
                            for (int i = 0; i < result.length; ++i) {
                                hello.addReceiver(result[i].getName());
                                // log(result[i].getName().getLocalName());
                            }
                            hello.setContent(MESSAGE_HELLO);
                            myAgent.send(hello);
                            reset(30000); // ustawia częstotliwość odświeżania
                            // na 30s
                        }
                    } catch (FIPAException fe) {
                        fe.printStackTrace();
                    }
                }
            });
            addBehaviour(new MediatorListener());
            addBehaviour(new ReservationListener());
            addBehaviour(new CancelReservationListener());
        } else {
            // Zakończ swoje działanie
            log("Niewłaściwe parametry");
            doDelete();
        }
        super.setup();
    }

    @Override
    protected void takeDown() {
        DFAgentDescription template = new DFAgentDescription();
        ServiceDescription sd = new ServiceDescription();
        sd.setType(MediatorAgent.TYPE);
        template.addServices(sd);
        try {
            DFAgentDescription[] result = DFService.search(this, template);
            ACLMessage goodbye = new ACLMessage(ACLMessage.INFORM);
            goodbye.setContent(MESSAGE_GOODBYE);
            for (DFAgentDescription description : result) {
                goodbye.addReceiver(description.getName());
            }
            send(goodbye);
        } catch (FIPAException fe) {
            fe.printStackTrace();
        }
        super.takeDown();
    }

    /**
     * Inner class MediatorListener.
     */
    private class MediatorListener extends CyclicBehaviour {

        // zapytanie od mediatora
        private OrderRequest req;
        // odpowiedź zwrotna
        private ACLMessage replyMsg;
        // lista pokoi spełniających kryteria klienta
        private ArrayList<Room> accurateRooms;

        @Override
        public void action() {
            try {
                accurateRooms = new ArrayList<>();
                // Słuchaj zapytań od mediatorów
                MessageTemplate mt = MessageTemplate.and(MessageTemplate.and(
                        MessageTemplate.MatchPerformative(ACLMessage.REQUEST),
                        MessageTemplate.MatchLanguage("JavaSerialization")),
                        MessageTemplate.MatchConversationId("offer-search"));
                ACLMessage reqMsg = myAgent.receive(mt);
                if (reqMsg != null) {
                    log("Mediator " + reqMsg.getSender().getLocalName()
                            + " prosi o pokoje.");
                    req = (OrderRequest) reqMsg.getContentObject();
                    replyMsg = reqMsg.createReply();

                    // Skompletuj pokoje, które spełniają wymagania
                    if (cityName.equals(req.getCity())
                            && stars >= req.getHotelStarsMin()
                            && stars <= req.getHotelStarsMax()) {
                        for (Room room : rooms) {
                            if (room.getPlaceFor() >= req.getRoomFor()
                                    && room.getPrice() <= req.getMaxPrice()) {

                                boolean canBeAdded = true;
                                Calendar calendar = Calendar.getInstance();
                                calendar.setTime(req.getDate());
                                for (int dayNr = 0; dayNr < req.getDays(); dayNr++) {
                                    Map<AID, Room> booked = roomsBooked.get(calendar.getTime());
                                    if (booked != null) {
                                        Iterator<Room> it = booked.values().iterator();
                                        while (it.hasNext()) {
                                            if (it.next().equals(room)) {
                                                canBeAdded = false;
                                                break;
                                            }
                                        }
                                        if (!canBeAdded)
                                            break;
                                    }
                                    calendar.add(Calendar.DAY_OF_YEAR, 1);
                                }
                                if (canBeAdded) {
                                    accurateRooms.add(room);
                                }
                            }
                        }

                    } else {
                        // Zapytanie skierowane do hotelu z innego miasta
                        // lub o innym standardzie
                        accurateRooms = null;
                    }

                    // Wyślij listę hoteli mediatorowi
                    log("Wysyłam listę pokoi do "
                            + reqMsg.getSender().getLocalName() + ".");
                    OrderResponse myResp = new OrderResponse(getAID(),
                            cityName, stars, accurateRooms);
                    replyMsg.setPerformative(ACLMessage.PROPOSE);
                    replyMsg.setContentObject(myResp);
                    myAgent.send(replyMsg);
                } else {
                    block();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private class ReservationListener extends CyclicBehaviour {

        // pokój do zarezerwowania
        private Reservation reservation;
        // ID agenta żądającego
        private AID sender;
        // wiadomość odpowiedź
        private ACLMessage replMsg;

        @Override
        public synchronized void action() {
            try {
                MessageTemplate mt = MessageTemplate.and(MessageTemplate.and(
                        MessageTemplate.MatchPerformative(ACLMessage.REQUEST),
                        MessageTemplate.MatchLanguage("JavaSerialization")),
                        MessageTemplate.MatchConversationId("hotel-reserve"));
                ACLMessage reqMsg = myAgent.receive(mt);
                if (reqMsg != null) {
                    log("Klient " + reqMsg.getSender().getLocalName()
                            + " próbuje zarezerwować pokój.");
                    replMsg = reqMsg.createReply();
                    reservation = (Reservation) reqMsg.getContentObject();
                    sender = reqMsg.getSender();

                    Boolean isAvailable = true;

                    Calendar calendar = Calendar.getInstance();
                    calendar.setTime(reservation.getDate());
                    for (int dayNr = 0; dayNr < reservation.getDays(); dayNr++) {
                        Map<AID, Room> booked = roomsBooked.get(calendar.getTime());
                        if (booked != null) {
                            for (Room room : booked.values()) {
                                if (room.getRoomId() == reservation.getRoom().getRoomId()) {
                                    isAvailable = false;
                                    break;
                                }
                            }
                            if (!isAvailable) {
                                break;
                            }
                        }
                        calendar.add(Calendar.DAY_OF_YEAR, 1);
                    }

                    if (isAvailable) {
                        replMsg.setPerformative(ACLMessage.AGREE);
                        log("Pokój o który prosi klient "
                                + reqMsg.getSender().getLocalName()
                                + " jest wolny. Akceptuję.");
                        for (Room roomIter : rooms) {
                            if (roomIter.getRoomId() == reservation.getRoom().getRoomId()) {
                                break;
                            }
                        }
                        calendar.setTime(reservation.getDate());
                        for (int dayNr = 0; dayNr < reservation.getDays(); dayNr++) {
                            Map<AID, Room> booked = roomsBooked.get(calendar.getTime());
                            if (booked == null) {
                                booked = new HashMap<AID, Room>();
                                roomsBooked.put(calendar.getTime(), booked);
                            }
                            booked.put(sender, reservation.getRoom());
                            calendar.add(Calendar.DAY_OF_YEAR, 1);
                        }
                    } else {
                        log("Pokój o który prosi klient "
                                + reqMsg.getSender().getLocalName()
                                + " jest zajęty. Odmawiam.");
                        replMsg.setPerformative(ACLMessage.REFUSE);
                    }
                    myAgent.send(replMsg);
                } else {
                    block();
                }
            } catch (UnreadableException e) {
                System.err.println(getLocalName() + " catched exception "
                        + e.getMessage());
            }
        }
    }

    private class CancelReservationListener extends CyclicBehaviour {

        // ID agenta żądającego
        private AID sender;
        // wiadomość odpowiedź
        private ACLMessage replMsg;

        @Override
        public void action() {
            try {
                MessageTemplate mt = MessageTemplate.and(MessageTemplate.and(MessageTemplate.MatchPerformative(ACLMessage.REQUEST),
                        MessageTemplate.MatchLanguage("JavaSerialization")),
                        MessageTemplate.MatchConversationId(ACTION_CANCEL_RESERVATION));
                ACLMessage reqMsg = myAgent.receive(mt);
                if (reqMsg != null) {
                    replMsg = reqMsg.createReply();
                    Reservation reservation = (Reservation) reqMsg.getContentObject();
                    sender = reqMsg.getSender();
                    log("Klient " + reqMsg.getSender().getLocalName()
                            + " anuluje rezerwację pokoju "
                            + reservation.getRoom().toString());

                    Calendar calendar = Calendar.getInstance();
                    calendar.setTime(reservation.getDate());
                    for (int dayNr = 0; dayNr < reservation.getDays(); dayNr++) {
                        Map<AID, Room> booked = roomsBooked.get(calendar.getTime());
                        if (booked != null) {
                            Room reservedRoom = booked.get(sender);
                            if (reservedRoom == null
                                    || !reservedRoom.equals(reservation.getRoom())) {
                                log("Klient "
                                        + reqMsg.getSender().getLocalName()
                                        + " nie miał zarezerwowanego pokoju "
                                        + reservation.getRoom().toString()
                                        + " dnia "
                                        + new SimpleDateFormat().format(calendar.getTime()));
                            }
                        }
                        calendar.add(Calendar.DAY_OF_YEAR, 1);
                    }

                    replMsg.setPerformative(ACLMessage.CONFIRM);
                    myAgent.send(replMsg);

                    // ACLMessage newRoomAvailable = new ACLMessage(
                    // ACLMessage.INFORM);
                    // for (AID client : roomsBooked.keySet()) {
                    // if (roomsBooked.get(client).getPlaceFor() <= reservation
                    // .getRoom().getPlaceFor()
                    // && roomsBooked.get(client).getPrice() >= reservation
                    // .getRoom().getPrice()) {
                    // newRoomAvailable.addReceiver(client);
                    // }
                    // }
                    // if (newRoomAvailable.getAllReceiver().hasNext()) {
                    // log("Znalazłem klientów, którzy mogą być zainteresowani pokojem.");
                    // newRoomAvailable
                    // .setContentObject(reservation.getRoom());
                    // newRoomAvailable.setLanguage("JavaSerialization");
                    // newRoomAvailable.setConversationId("newRoomAvailable");
                    // myAgent.send(newRoomAvailable);
                    // } else {
                    // log("Nie znalazłem klientów, którzy byliby zainteresowani tym pokojem.");
                    // }
                } else {
                    block();
                }
            } catch (UnreadableException e) {
                System.err.println(getLocalName() + " catched exception "
                        + e.getMessage());
            }
        }
    }

    @Override
    protected synchronized void onDateChanged(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.DAY_OF_MONTH, -1);
        date = calendar.getTime();
        Map<AID, Room> booked = roomsBooked.get(date);
        if (booked == null) {
            //roomsBooked.put(date, new HashMap<AID, Room>());
            return;
        }
        ACLMessage message = new ACLMessage(ACLMessage.INFORM);
        Map<AID, Room> nextDayBooked = roomsBooked.get(nextDay(date));
        for (AID client : booked.keySet()) {
            if (nextDayBooked == null || nextDayBooked.get(client) == null) {
                message.addReceiver(client);
            }
        }
        message.setConversationId(ClientAgent.ACTION_GO_HOME);
        send(message);
        roomsBooked.remove(date);
    }

    private Date nextDay(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.DAY_OF_YEAR, 1);
        return calendar.getTime();
    }
}
